/*
 * Project:  NextGIS Mobile
 * Purpose:  Mobile GIS for Android.
 * Author:   Dmitry Baryshnikov (aka Bishop), bishop.dev@gmail.com
 * Author:   NikitaFeodonit, nfeodonit@yandex.com
 * Author:   Stanislav Petriakov, becomeglory@gmail.com
 * *****************************************************************************
 * Copyright (c) 2012-2016 NextGIS, info@nextgis.com
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package com.getbase.floatingactionbutton;

import android.annotation.SuppressLint;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.LinearGradient;
import android.graphics.Paint;
import android.graphics.Paint.Style;
import android.graphics.Rect;
import android.graphics.Shader;
import android.graphics.Shader.TileMode;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.LayerDrawable;
import android.graphics.drawable.ShapeDrawable;
import android.graphics.drawable.ShapeDrawable.ShaderFactory;
import android.graphics.drawable.StateListDrawable;
import android.graphics.drawable.shapes.OvalShape;
import android.os.Build;
import android.os.Build.VERSION_CODES;
import android.support.annotation.ColorRes;
import android.support.annotation.DimenRes;
import android.support.annotation.DrawableRes;
import android.support.annotation.IntDef;
import android.support.annotation.NonNull;
import android.util.AttributeSet;
import android.view.View;
import android.widget.ImageButton;
import android.widget.TextView;

import com.nextgis.maplibui.util.ControlHelper;
import com.nextgis.mobile.R;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;


public class FloatingActionButton
        extends ImageButton
{

    public static final int SIZE_NORMAL = 0;
    public static final int SIZE_MINI   = 1;


    @Retention(RetentionPolicy.SOURCE)
    @IntDef({SIZE_NORMAL, SIZE_MINI})
    public @interface FAB_SIZE
    {
    }


    int    mColorNormal;
    int    mColorPressed;
    int    mColorDisabled;
    String mTitle;
    @DrawableRes
    private int      mIcon;
    private Drawable mIconDrawable;
    private int      mSize;

    private float mCircleSize;
    private float mShadowRadius;
    private float mShadowOffset;
    private int   mDrawableSize;
    boolean mStrokeVisible;


    public FloatingActionButton(Context context)
    {
        this(context, null);
    }


    public FloatingActionButton(
            Context context,
            AttributeSet attrs)
    {
        super(context, attrs);
        init(context, attrs);
    }


    public FloatingActionButton(
            Context context,
            AttributeSet attrs,
            int defStyle)
    {
        super(context, attrs, defStyle);
        init(context, attrs);
    }


    void init(
            Context context,
            AttributeSet attributeSet)
    {
        TypedArray attr = context.obtainStyledAttributes(attributeSet, R.styleable.FloatingActionButton, 0, 0);
        mColorNormal = attr.getColor(R.styleable.FloatingActionButton_fab_colorNormal, ControlHelper.getColor(context, R.attr.colorPrimary));
        mColorPressed = attr.getColor(R.styleable.FloatingActionButton_fab_colorPressed, ControlHelper.getColor(context, R.attr.colorAccent));
        mColorDisabled = attr.getColor(R.styleable.FloatingActionButton_fab_colorDisabled, getColor(android.R.color.darker_gray));
        mSize = attr.getInt(R.styleable.FloatingActionButton_fab_size, SIZE_NORMAL);
        mIcon = attr.getResourceId(R.styleable.FloatingActionButton_fab_icon, 0);
        mTitle = attr.getString(R.styleable.FloatingActionButton_fab_title);
        mStrokeVisible = attr.getBoolean(R.styleable.FloatingActionButton_fab_stroke_visible, true);
        attr.recycle();

        updateCircleSize();
        mShadowRadius = getDimension(R.dimen.fab_shadow_radius);
        mShadowOffset = getDimension(R.dimen.fab_shadow_offset);
        updateDrawableSize();

        updateBackground();
    }


    private void updateDrawableSize()
    {
        mDrawableSize = (int) (mCircleSize + 2 * mShadowRadius);
    }


    private void updateCircleSize()
    {
        mCircleSize = getDimension(
                mSize == SIZE_NORMAL ? R.dimen.fab_size_normal : R.dimen.fab_size_mini);
    }


    public void setSize(
            @FAB_SIZE
            int size)
    {
        if (size != SIZE_MINI && size != SIZE_NORMAL) {
            throw new IllegalArgumentException("Use @FAB_SIZE constants only!");
        }

        if (mSize != size) {
            mSize = size;
            updateCircleSize();
            updateDrawableSize();
            updateBackground();
        }
    }


    @FAB_SIZE
    public int getSize()
    {
        return mSize;
    }


    public void setIcon(
            @DrawableRes
            int icon)
    {
        if (mIcon != icon) {
            mIcon = icon;
            mIconDrawable = null;
            updateBackground();
        }
    }


    public void setIconDrawable(
            @NonNull
            Drawable iconDrawable)
    {
        if (mIconDrawable != iconDrawable) {
            mIcon = 0;
            mIconDrawable = iconDrawable;
            updateBackground();
        }
    }


    /**
     * @return the current Color for normal state.
     */
    public int getColorNormal()
    {
        return mColorNormal;
    }


    public void setColorNormalResId(
            @ColorRes
            int colorNormal)
    {
        setColorNormal(getColor(colorNormal));
    }


    public void setColorNormal(int color)
    {
        if (mColorNormal != color) {
            mColorNormal = color;
            updateBackground();
        }
    }


    /**
     * @return the current color for pressed state.
     */
    public int getColorPressed()
    {
        return mColorPressed;
    }


    public void setColorPressedResId(
            @ColorRes
            int colorPressed)
    {
        setColorPressed(getColor(colorPressed));
    }


    public void setColorPressed(int color)
    {
        if (mColorPressed != color) {
            mColorPressed = color;
            updateBackground();
        }
    }


    /**
     * @return the current color for disabled state.
     */
    public int getColorDisabled()
    {
        return mColorDisabled;
    }


    public void setColorDisabledResId(
            @ColorRes
            int colorDisabled)
    {
        setColorDisabled(getColor(colorDisabled));
    }


    public void setColorDisabled(int color)
    {
        if (mColorDisabled != color) {
            mColorDisabled = color;
            updateBackground();
        }
    }


    public void setStrokeVisible(boolean visible)
    {
        if (mStrokeVisible != visible) {
            mStrokeVisible = visible;
            updateBackground();
        }
    }


    public boolean isStrokeVisible()
    {
        return mStrokeVisible;
    }


    int getColor(
            @ColorRes
            int id)
    {
        return getResources().getColor(id);
    }


    float getDimension(
            @DimenRes
            int id)
    {
        return getResources().getDimension(id);
    }


    public void setTitle(String title)
    {
        mTitle = title;
        TextView label = getLabelView();
        if (label != null) {
            label.setText(title);
        }
    }


    TextView getLabelView()
    {
        return (TextView) getTag(R.id.fab_label);
    }


    public String getTitle()
    {
        return mTitle;
    }


    @Override
    protected void onMeasure(
            int widthMeasureSpec,
            int heightMeasureSpec)
    {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        setMeasuredDimension(mDrawableSize, mDrawableSize);
    }


    void updateBackground()
    {
        final float strokeWidth = getDimension(R.dimen.fab_stroke_width);
        final float halfStrokeWidth = strokeWidth / 2f;

        LayerDrawable layerDrawable = new LayerDrawable(
                new Drawable[] {
                        getResources().getDrawable(
                                mSize == SIZE_NORMAL
                                ? R.drawable.fab_bg_normal
                                : R.drawable.fab_bg_mini),
                        createFillDrawable(strokeWidth),
                        createOuterStrokeDrawable(strokeWidth),
                        getIconDrawable()});

        int iconOffset = (int) (mCircleSize - getDimension(R.dimen.fab_icon_size)) / 2;

        int circleInsetHorizontal = (int) (mShadowRadius);
        int circleInsetTop = (int) (mShadowRadius - mShadowOffset);
        int circleInsetBottom = (int) (mShadowRadius + mShadowOffset);

        layerDrawable.setLayerInset(
                1, circleInsetHorizontal, circleInsetTop, circleInsetHorizontal, circleInsetBottom);

        layerDrawable.setLayerInset(
                2, (int) (circleInsetHorizontal - halfStrokeWidth),
                (int) (circleInsetTop - halfStrokeWidth),
                (int) (circleInsetHorizontal - halfStrokeWidth),
                (int) (circleInsetBottom - halfStrokeWidth));

        layerDrawable.setLayerInset(
                3, circleInsetHorizontal + iconOffset, circleInsetTop + iconOffset,
                circleInsetHorizontal + iconOffset, circleInsetBottom + iconOffset);

        setBackgroundCompat(layerDrawable);
    }


    public Drawable getIconDrawable()
    {
        if (mIconDrawable != null) {
            return mIconDrawable;
        } else if (mIcon != 0) {
            return getResources().getDrawable(mIcon);
        } else {
            return new ColorDrawable(Color.TRANSPARENT);
        }
    }


    private StateListDrawable createFillDrawable(float strokeWidth)
    {
        StateListDrawable drawable = new StateListDrawable();
        drawable.addState(
                new int[] {-android.R.attr.state_enabled},
                createCircleDrawable(mColorDisabled, strokeWidth));
        drawable.addState(
                new int[] {android.R.attr.state_pressed},
                createCircleDrawable(mColorPressed, strokeWidth));
        drawable.addState(new int[] {}, createCircleDrawable(mColorNormal, strokeWidth));
        return drawable;
    }


    private Drawable createCircleDrawable(
            int color,
            float strokeWidth)
    {
        int alpha = Color.alpha(color);
        int opaqueColor = opaque(color);

        ShapeDrawable fillDrawable = new ShapeDrawable(new OvalShape());

        final Paint paint = fillDrawable.getPaint();
        paint.setAntiAlias(true);
        paint.setColor(opaqueColor);

        Drawable[] layers = {
                fillDrawable, createInnerStrokesDrawable(opaqueColor, strokeWidth)};

        LayerDrawable drawable = alpha == 255 || !mStrokeVisible
                                 ? new LayerDrawable(layers)
                                 : new TranslucentLayerDrawable(alpha, layers);

        int halfStrokeWidth = (int) (strokeWidth / 2f);
        drawable.setLayerInset(
                1, halfStrokeWidth, halfStrokeWidth, halfStrokeWidth, halfStrokeWidth);

        return drawable;
    }


    private static class TranslucentLayerDrawable
            extends LayerDrawable
    {
        private final int mAlpha;


        public TranslucentLayerDrawable(
                int alpha,
                Drawable... layers)
        {
            super(layers);
            mAlpha = alpha;
        }


        @Override
        public void draw(Canvas canvas)
        {
            Rect bounds = getBounds();
            canvas.saveLayerAlpha(
                    bounds.left, bounds.top, bounds.right, bounds.bottom, mAlpha,
                    Canvas.ALL_SAVE_FLAG);
            super.draw(canvas);
            canvas.restore();
        }
    }


    private Drawable createOuterStrokeDrawable(float strokeWidth)
    {
        ShapeDrawable shapeDrawable = new ShapeDrawable(new OvalShape());

        final Paint paint = shapeDrawable.getPaint();
        paint.setAntiAlias(true);
        paint.setStrokeWidth(strokeWidth);
        paint.setStyle(Style.STROKE);
        paint.setColor(Color.BLACK);
        paint.setAlpha(opacityToAlpha(0.02f));

        return shapeDrawable;
    }


    private int opacityToAlpha(float opacity)
    {
        return (int) (255f * opacity);
    }


    private int darkenColor(int argb)
    {
        return adjustColorBrightness(argb, 0.9f);
    }


    private int lightenColor(int argb)
    {
        return adjustColorBrightness(argb, 1.1f);
    }


    private int adjustColorBrightness(
            int argb,
            float factor)
    {
        float[] hsv = new float[3];
        Color.colorToHSV(argb, hsv);

        hsv[2] = Math.min(hsv[2] * factor, 1f);

        return Color.HSVToColor(Color.alpha(argb), hsv);
    }


    private int halfTransparent(int argb)
    {
        return Color.argb(
                Color.alpha(argb) / 2, Color.red(argb), Color.green(argb), Color.blue(argb));
    }


    private int opaque(int argb)
    {
        return Color.rgb(
                Color.red(argb), Color.green(argb), Color.blue(argb));
    }


    private Drawable createInnerStrokesDrawable(
            final int color,
            float strokeWidth)
    {
        if (!mStrokeVisible) {
            return new ColorDrawable(Color.TRANSPARENT);
        }

        ShapeDrawable shapeDrawable = new ShapeDrawable(new OvalShape());

        final int bottomStrokeColor = darkenColor(color);
        final int bottomStrokeColorHalfTransparent = halfTransparent(bottomStrokeColor);
        final int topStrokeColor = lightenColor(color);
        final int topStrokeColorHalfTransparent = halfTransparent(topStrokeColor);

        final Paint paint = shapeDrawable.getPaint();
        paint.setAntiAlias(true);
        paint.setStrokeWidth(strokeWidth);
        paint.setStyle(Style.STROKE);
        shapeDrawable.setShaderFactory(
                new ShaderFactory()
                {
                    @Override
                    public Shader resize(
                            int width,
                            int height)
                    {
                        return new LinearGradient(
                                width / 2, 0, width / 2, height, new int[] {
                                topStrokeColor,
                                topStrokeColorHalfTransparent,
                                color,
                                bottomStrokeColorHalfTransparent,
                                bottomStrokeColor}, new float[] {0f, 0.2f, 0.5f, 0.8f, 1f},
                                TileMode.CLAMP);
                    }
                });

        return shapeDrawable;
    }


    @SuppressWarnings("deprecation")
    @SuppressLint("NewApi")
    private void setBackgroundCompat(Drawable drawable)
    {
        if (Build.VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN) {
            setBackground(drawable);
        } else {
            setBackgroundDrawable(drawable);
        }
    }


    @Override
    public void setVisibility(int visibility)
    {
        TextView label = getLabelView();
        if (label != null) {
            label.setVisibility(visibility);
        }

        super.setVisibility(visibility);
    }


    @Override
    protected boolean onSetAlpha(int alpha)
    {
        if (Build.VERSION.SDK_INT < VERSION_CODES.LOLLIPOP) {
            if (alpha == 0) {
                setVisibility(View.GONE);
            } else {
                setVisibility(View.VISIBLE);
            }
        } else {
            if (alpha == 0) {
                setEnabled(false);
            } else {
                setEnabled(true);
            }
        }
        return super.onSetAlpha(alpha);
    }
}
